/*
 * Copyright 2013 The Scripps Research Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package edu.scripps.fl.entrez;

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.Node;
import org.dom4j.io.DOMReader;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import edu.scripps.fl.entrez.internal.ESearchHandler;
import edu.scripps.fl.entrez.internal.EUtilsWebSessionBase;

public class EUtilsWeb extends EUtilsWebSessionBase {

	private static final Logger log = LoggerFactory.getLogger(EUtilsWeb.class);
	
	public static EUtilsWeb newInstance() throws EUtilsException {
		String tool = System.getProperty("entrez.tool");
		String email = System.getProperty("entrez.email");
		if( StringUtils.isEmpty(tool) )
			throw new EUtilsException("System property 'entrez.tool' is required by " + EUtilsWeb.class.getName());
		if( StringUtils.isEmpty(email) )
			throw new EUtilsException("System property 'entrez.email' is required by " + EUtilsWeb.class.getName());
		return new EUtilsWeb(tool, email);
	}
	
	public EUtilsWeb(String tool, String userEmail) {
		super(tool, userEmail);
	}

	public Callable<Document> getDocumentCallable(final String url, final Object... params) {
		return new Callable<Document>() {
			public Document call() throws EUtilsException {
				try {
					InputStream is = getInputStream(url, params);
					return getDocument(is);
				} catch (Exception ex) {
					throw new EUtilsException(String.format("Error fetching URL: %s with params: %s", url, params), ex);
				}
			}
		};
	}

	public Document getDocument(final String url, final Object... params) throws EUtilsException {
		List<Object> list = new ArrayList();
		list.addAll(Arrays.asList((Object[]) params));
		return getDocument(url, list);
	}

	public Document getDocument(final String url, final Collection<Object> params) throws EUtilsException {
		InputStream is = null;
		try {
			is = getInputStream(url, params);
			return getDocument(is);
		} catch (Exception ex) {
			if( ex instanceof EUtilsException)
				throw (EUtilsException) ex;
			else throw new EUtilsException(ex);
		}
		finally {
			IOUtils.closeQuietly(is);
		}
	}

	public Document getDocument(InputStream in) throws EUtilsException {
		InputStream is = null;
		try {
			long start = System.currentTimeMillis();
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			DOMReader reader = new DOMReader();
			Document document = reader.read(builder.parse(new BufferedInputStream(in)));
			long end = System.currentTimeMillis();
			log.debug(String.format("Document parsed in %sms", end - start));
			if (DEBUGGING)
				new XMLWriter().write(document);
			// SAXReader reader = new SAXReader();
			// Document document = reader.read(in);
			Node node = document.selectSingleNode("/*/ERROR");
			if (node != null)
				throw new EUtilsException(String.format("<%s> response contained an error: %s", node.getDocument().getRootElement().getName(), node.getText()));
			return document;
		} catch (Exception ex) {
			if( ex instanceof EUtilsException)
				throw (EUtilsException) ex;
			else
				throw new EUtilsException("Error parsing Entrez XML response", ex);
		}
		finally {
			IOUtils.closeQuietly(is);
		}
	}

	public Set<String> getDatabases() throws EUtilsException {
		try {
			Document document = getDocument(EINFO_URL);
			List<Node> list = document.selectNodes("/eInfoResult/DbList/DbName", ".");
			Set dbs = new HashSet(list.size());
			for (Node node : list)
				dbs.add(node.getText());
			return dbs;
		} catch (Exception ex) {
			throw new EUtilsException("Error obtaining list of Entrez databases",ex);
		}
	}

	public Collection<Long> getIds(String query, String db, Collection<Long> ids) throws EUtilsException {
		return getIds(query, db, ids, 100000, -1);
	}

	public List<Long> getIds(String query, String db) throws EUtilsException {
		return (List<Long>) getIds(query, db, new ArrayList<Long>(), 100000, -1);
	}
	
	public Collection<Long> getIds(String query, String db, Collection<Long> ids, int chunkSize, int limit) throws EUtilsException {
		ESearchHandler handler = new ESearchHandler(ids, limit);
		try {
			int retStart = 0;
			SAXParserFactory factory = SAXParserFactory.newInstance();
			SAXParser saxParser = factory.newSAXParser();
			InputStream is = getInputStream(ESEARCH_URL, "db", db, "term", query, "retstart", retStart, "retmax", chunkSize, "usehistory",
					"y");
			while (true) {
				saxParser.parse(is, handler);
				if ((handler.getRetStart() + handler.getRetMax()) < (handler.getCount() - 1)) {
					retStart += handler.getRetMax();
					is = getInputStream(ESEARCH_URL, "db", db, "term", query, "retstart", retStart, "retmax", "" + chunkSize, "usehistory",
							"y", "QueryKey", handler.getQueryKey(), "WebEnv", handler.getWebEnv());
				} else
					break;
			}
			return ids;
		}
		catch (SAXException ex) {
			if( handler.isLimitReached() )
				return ids;
			else
				throw new EUtilsException("Error parsing ESearch response", ex);
		}
		catch (Exception ex) {
			throw new EUtilsException("ESearch error fetching ids: " + query, ex);
		}
	}

	public Document getSummary(Object id, String db) throws EUtilsException {
		return getDocument(ESUMMARY_URL, "db", db, "id", id, "retmode", "xml");
	}

	public InputStream getSummaries(Collection<Long> ids, String db) throws EUtilsException {
		try {
			String idStr = StringUtils.join(ids, ",");
			return getInputStream(ESUMMARY_URL, "db", db, "id", idStr, "retmode", "xml");
		} catch (Exception ex) {
			throw new EUtilsException(String.format("ESummary error fetching %s ids: %s", db, ids), ex);
		}
	}

	public InputStream getSummaries(EntrezHistoryKey key) throws EUtilsException {
		try {
			return getInputStream(ESUMMARY_URL, "db", key.getDatabase(), "WebEnv", key.getWebEnv(), "query_key", key.getQueryKey(),
					"retmode", "xml");
		} catch (Exception ex) {
			throw new EUtilsException(String.format("ESummary error fetching Entrez Key"),ex);
		}
	}

	public Document getSummariesAsDocument(Collection<Long> ids, String db) throws EUtilsException {
		return getDocument(getSummaries(ids, db));
	}

	public EntrezHistoryKey postIds(String db, Long... ids) throws EUtilsException {
		return postIds(db, Arrays.asList(ids));
	}

	public EntrezHistoryKey postIds(String db, Collection<Long> ids) throws EUtilsException {
		String idStr = StringUtils.join(ids, ",");
		Document doc = getDocument(EPOST_URL, "tool", getTool(), "db", db, "id", idStr);
		String queryKey = doc.valueOf("/ePostResult/QueryKey/text()");
		String webEnv = doc.valueOf("/ePostResult/WebEnv/text()");
		EntrezHistoryKey key = new EntrezHistoryKey(db, webEnv, queryKey);
		return key;
	}
}